<?php
// FileFind - ftp/shares indexing and search php engine,
// http://project.ffind.ru
//
// Module: lib/class.host.php
// Description: Host representing class
// Copyright (c) 2003-2008 Rage Steel, www.gt2.ru
// ----------------------------------------------
// $Id$

require_once('./lib/class.sql.php');
require_once('./lib/lib.main.php');

function log_host_action($host_id, $hact_type, $hact_datetime = '') {
  $sq = new CSqlQ('tbl_hact');
  if ($hact_datetime == '') {
    $sq->add_calc_field('hact_datetime', 'NOW()');
  } else {
    $sq->add_value('hact_datetime', $hact_datetime);
  }
  $sq->add_value('host_id', (int)$host_id);
  $sq->add_value('hact_type', (int)$hact_type);

  $sq->exec_insert();
} // log_host_action

// Класс для работы с хостами
class CHost {
  // Идентификатор хоста
  var $host_id = -1;
  // Тип хоста, HOST_TYPE_* в lib.main
  var $host_type = -1;
  // Имя хоста
  var $host_name = '';
  // IP-адрес хоста
  var $host_ip = '0.0.0.0';
  // Владелец хоста
  var $host_owner = '';
  // Параметры хоста
  var $host_opt = 0;
  // Последняя ошибка подключения
  var $conn_error;
  // MySQL Дата последнего успешного подключения
  var $conn_ok = NULL;
  // MySQL Дата последнего подключения
  var $conn_last = NULL;
  
  /**
   * Дата последней успешной индексации
   *
   * @var string sql_datetime
   */
  var $crawl_last = NULL;
  
  // MySQL Дата добавления узла
  var $date_add = NULL;
  // Идентификатор корневого каталога хоста
  var $root_dir_id = -1;

  /**
   * «Сырой» листинг содержимого ftp
   *
   * @var array
   */
  var $rawlist;
  
  /**
   * Количество элементов в сыром листинге, сразу после его получения
   *
   * @var integer
   */
  var $rawlist_count;
  
  // Массив имён шар для индексации
  var $shares;
  // Результат выполнения net view в connect share check
  var $net_view_result;
  // Дата начала процедуры подключения
  var $conn_datetime;
  // Откуда продолжать обход (для ftp)
  var $resume_path = null;

  /**
   * Принадлежит ли этот узел текушей группе
   *
   * @var boolean
   */
  var $in_current_group = true;

  /**
   * Идентификатор текущей группы
   *
   * @var integer
   */
  var $group_id;

  // Конструктор. Если есть идентификатор хоста --- выполняется его загрузка
  function CHost($host_id = -1) {
    $this->host_id = $host_id;
    $this->load();
  } // CHost
  
  // Создать объект запроса с уже установленным host_id
  function host_tbl() {
    $sq = new CSqlQ('tbl_hosts');
    $sq->add_value('host_id', $this->host_id);
    $sq->fill_where(true);
    return $sq;
  } // host_tbl

  // Загрузка данных хоста
  // Или проверка существования хоста, если $this->host_id != -1 и $host_name != ''
  // Если загрузка не удалась, то выставлят совой host_id в -1
  function load($host_name = '', $ip_search = false) {
    global $sqlconn;

    if ($this->host_id == -1) {
      if ($host_name == '') {
        return false;
      }
    }
    
    $sq = new CSqlQ('tbl_hosts');

    if ($this->host_id != -1) {
      $sq->add_value('host_id', $this->host_id);
      $sq->fill_where(true);
    } else {
      $host_name = $sqlconn->escape($host_name, true);

      $where = '`host_name` LIKE "' . $host_name . '"';
      if ($ip_search) {
        $where .= ' OR `host_ip` = INET_ATON("' . $host_name . '")';
      }
      $sq->where = $where;
    }
    
    $sq->add_fields(array('host_id', 'group_id', 'host_type', 'host_name', 
      'host_owner', 'date_add', 'host_opt', 'host_status',
      'lock_pid', 'lock_datetime', 'root_dir_id', 'conn_error',
      'conn_ok', 'conn_last', 'crawl_last', 'resume_path'));
    $sq->add_calc_field('host_ip', 'INET_NTOA(`host_ip`)');
    $sq->exec_select();
    if ($sq->num_rows() == 1) {
      $result = $this->load_sq_data($sq);
    } else {
      $result = false;
      $this->host_id = -1;
    }
    unset($sq);

    return $result;
  } // load

  // Загрузка данных из SQL Select запроса
  // Порядок следования полей должен соответствовать ->load
  function load_sq_data(&$sq) {
    global $host_group_id;

    list(
      $this->host_id,
      $this->group_id,
      $this->host_type,
      $this->host_name,
      $this->host_owner,
      $this->date_add,
      $this->host_opt,
      $this->host_status,
      $this->lock_pid,
      $this->lock_datetime,
      $this->root_dir_id,
      $this->conn_error,
      $this->conn_ok,
      $this->conn_last,
      $this->crawl_last,
      $this->resume_path,
      $this->host_ip
      ) = $sq->fetch_row();

    if (isset($host_group_id)) {
      $this->in_current_group = ($host_group_id == $this->group_id);
    }
    
    return true;
  } // load_sq_data

  // Сохранение данных о хосте в таблицу
  // Гы, похоже эта процедура нигде не вызывается пока-что.
  function update() {
    $sq = $this->host_tbl();

    $sq->add_value('host_type',   $this->host_type);
    $sq->add_value('host_name',   $this->host_name);
    $sq->add_value('host_owner',  $this->host_owner);
    $sq->add_value('host_opt',    $this->host_opt);
    $sq->add_value('root_dir_id', $this->root_dir_id);
    $sq->add_value('crawl_last',  $this->crawl_last);
    $sq->add_calc_field('host_ip', 'INET_ATON("' . $this->host_ip . '")');
    $sq->exec_update();
    unset($sq);
  } // update

  // Проверка, является-ли хост новым
  // Возвращает истину если отсутствует conn_crawl
  function is_new() {
    return is_null($this->crawl_last);
  } // is_new

  // Создание нового хоста
  function create($host_name, $host_type = HOST_TYPE_FTP, $host_ip = '', $host_owner = '(unknown)') {
    global $sqlconn, $host_group_id;

    $this->host_name = $host_name;
    $this->host_type = $host_type;
    $this->host_owner = $host_owner;
    $this->host_ip = $host_ip;

    // Добавляем хост в таблицу
    $sq = new CSqlQ('tbl_hosts');
    $sq->add_value('host_name', $host_name);
    $sq->add_value('host_type', $host_type);
    $sq->add_value('host_owner', $host_owner);

    if (isset($host_group_id)) {
      $sq->add_value('group_id', $host_group_id);
    }

    $sq->add_value('conn_error', $this->conn_error);
    $sq->add_value('conn_ok', $this->conn_ok);
    $sq->add_value('conn_last', $this->conn_last);

    $sq->add_calc_field('host_ip', 'INET_ATON("' . $sqlconn->escape($this->host_ip) . '")');
    $sq->add_calc_field('date_add', 'NOW()');
    $sq->exec_insert();
    $this->host_id = $sq->last_insert_id();

    $this->create_root_dir();
    // Записываем dir_id корневого каталога в host
    $this->update_field('root_dir_id', $this->root_dir_id);
  } // create

  // Перевод в активное состояние (из неактивного)
  function make_active() {
    if ($this->host_opt & 1 == 0) {
      return false;
    }

    $this->create_root_dir();

    $sq = $this->host_tbl();
    $sq->add_value('root_dir_id', $this->root_dir_id);
    $sq->add_value('host_opt', $this->host_opt & ~ (1 | 2 | 4));
    $sq->add_value('crawl_last', null);

    $sq->exec_update();

    return true;
  } // make_active

  // Создание корневого каталога
  function create_root_dir() {
    // Создаем корневой каталог
    $sq = new CSqlQ('tbl_dirs');
    $sq->add_value('host_id', $this->host_id);
    $sq->add_value('dir_name', '');
    $sq->exec_insert();
// names ->
//    // Увеличим количество имён корневого каталога, с name_id = 0
//    $sq = new CSqlQ('tbl_names');
//    $sq->add_value('name_id', 0);
//    $sq->fill_where(true);
//    $sq->add_calc_field('name_cnt', '`name_cnt` + 1');
//    $sq->exec_update();
//
//    if ($sq->affected_rows() < 1) {
//      // Если ничего не обновилось, значит просто нужно добавить запись с name_id = 0
//      $sq = new CSqlQ('tbl_names');
//      $sq->add_value('name_id', 0);
//      $sq->add_value('name_txt', '');
//      $sq->add_value('name_cnt', 1);
//      $sq->exec_insert();
//    }
// <- names

    return $this->root_dir_id = $sq->last_insert_id();
  } // create_root_dir

  // Проверка подключения к хосту
  function check($host_name, $host_type = HOST_TYPE_FTP, $host_owner = '(unknown)') {
    $this->host_name = $host_name;
    $this->host_type = $host_type;
    $this->host_owner = $host_owner;

    return $this->connect(1);
  } // check

  // Установка IP-адреса хоста
  function set_ip($host_ip) {
    global $sqlconn;
    $this->host_ip = $sqlconn->escape($host_ip);

    $sq = $this->host_tbl();
    $sq->add_calc_field('host_ip', 'INET_ATON("' . $this->host_ip . '")');
    $sq->exec_update();
    unset($sq);
  } // set_ip

  // Установка типа хоста
  function set_type($host_type) {
    $this->host_type = (int)$host_type;
    return $this->update_field('host_type', $this->host_type);
  } // set_type

  // Обновить значение одного поля в базе данных
  function update_field($field_name, $value) {
    $sq = $this->host_tbl();
    $sq->add_value($field_name, $value);
    $sq->exec_update();
    unset($sq);
  } // update_field

  // Уничтожить поля из таблицы по $host_id
  function kill_table_host($table_name) {
    $sq = new CSqlQ($table_name);
    $sq->add_value('host_id', $this->host_id);
    $sq->fill_where(true);
    $sq->exec_delete();
    unset($sq);
  } // kill_table_host

  // Удаление всех файлов хоста
  function kill_files() {
    // !!! А ещё тут нужно будет удалять и всю дополнительную информацию по файлам
    // - листинги архивов, данные из тэгов
    $this->kill_table_host('tbl_files');
  } // kill_files

  // Удаление всех каталогов хоста
  function kill_dirs() {
    $this->kill_table_host('tbl_dirs');
  } // kill_dirs

  // Удаление всех записей об индексации хоста
  function kill_conn() {
    $this->kill_table_host('tbl_conn');
    $this->kill_table_host('tbl_crawl');
  } // kill_conn

  // Удаление самого хоста из таблицы
  function kill_host() {
    $sq = $this->host_tbl();
    $sq->exec_delete();
    unset($sq);

    $this->host_id = -1;
  } // kill_host()

  // *** Присоединение к узлу и выполнение заданного действия
  // список действий:
  // 0 - ping с записью результата в таблицу
  // 1 - ping без записи результатов
  // 2 - получение содержимого ftp

  // Статус == 0 или > 100 - OnLine

  // Возможные ответы: (host_error_txt)

  function connect($action) {
    $result = 0;
    
    $this->conn_datetime = get_sql_datetime();

    $start_time = RS_time_passed();

    if (RS_is_ip($this->host_name) == -1) {
      // Если host_name не является ip-адресом
      $host_ip = gethostbyname($this->host_name);
      if ($host_ip == $this->host_name) {
        $result = 1;
      } else {
        // А вот тут можно попробовать по ip-адресу, может dns лежит
        $this->host_ip = $host_ip;
      }
    } else {
      $this->host_ip = $this->host_name;
    }

    if (0 == $result) {
      if (HOST_TYPE_FTP == $this->host_type) {
        $result = $this->connect_ftp($action);
      } elseif (HOST_TYPE_SHARE == $this->host_type) {
        $result = $this->connect_share($action);
      } else {
        $result = 2;
      }
    }

    $this->conn_error = $result;
    $this->conn_last = $this->conn_datetime;
    if (0 == $result) {
      $this->conn_ok = $this->conn_datetime;
    }

    if (1 == $action) {
      return $result;
    }
    $duration = (RS_time_passed() - $start_time) * 10;

    $sq = $this->host_tbl();
    $sq->add_calc_field('host_ip', 'INET_ATON("' . $this->host_ip . '")');
    $sq->add_value('conn_error', $result);
    

    if ($result == 0) {
      $sq->add_value('conn_ok', $this->conn_datetime);
    }
    $sq->add_value('conn_last', $this->conn_datetime);
    $sq->exec_update();
    unset($sq);

    $sq = new CSqlQ('tbl_conn');
    $sq->add_value('host_id', $this->host_id);
    $sq->add_value('conn_datetime', $this->conn_datetime);
    $sq->add_value('conn_action', $action);
    $sq->add_value('conn_dur', (int)$duration);
    $sq->add_value('conn_error', $result);
    $sq->exec_insert();
    unset($sq);

    return $result;
  } // connect

  // Присоеденится к ftp и выполнить требуемое действие
  function connect_ftp($action = 0) {
    global $cfg;
    if(!($ftp = @ftp_connect($this->host_ip))) {
      return 3;
    };

    if (2 == $action) {
      ftp_set_option($ftp, FTP_TIMEOUT_SEC, 20);
    }

    $result = 0;

    if (@ftp_login($ftp, 'anonymous', $cfg['ftp_login'])) {
      if ($pwd = ftp_pwd($ftp)) {
        if ($systype = ftp_systype($ftp)) {
          if ((0 == $action) || (1 == $action)) {
            if (is_array($this->rawlist = ftp_rawlist($ftp, '/'))) {
              // TODO попытка разбора содержимого каталога
            } else {
              $result = 107;
            }
          } elseif (2 == $action) {
            ftp_set_option($ftp, FTP_TIMEOUT_SEC, $cfg['ftp_lslr_timeout']);

            $get_root = true;
            if (!is_null($this->resume_path)) {
              $get_root = false;
              $result = $this->resume_ftp_listing($ftp);
              if (-1 == $result) {
                $get_root = true;
                echo "#";
              }
            }

            if ($get_root) {
              if (is_array($this->rawlist = ftp_rawlist($ftp, '/', true))) {
                // Успешно получили листинг
                $result = $this->check_ftp_listing($ftp);
                
                if (-1 == $result) {
                  // Значит пока мы не можем обработать листинг
                  $this->update_field('host_opt', $this->host_opt | 64);
                  $result = 0;
                } else {
                  $this->update_field('host_opt', $this->host_opt & (~64));
                }
              } else {
                $result = 108;
              }
            }
          }
        } else {
          $result = 106;
        }
      } else {
        $result = 105;
      }
    } else {
      $result = 104;
    }
    ftp_quit($ftp);

    $this->rawlist_count = count($this->rawlist);

    return $result;
  } // connect_ftp

  // Проверка существования шары с заданном именем на компе
  function is_share_exists($share_name) {
   if ($handle = @opendir("\\\\" . $this->host_name . "\\" . $share_name)) {
     closedir($handle);
     return true;
   } else {
     return false;
   }
  } // is_share_exists

  // Проверка наличия подключения к компу
  function connect_share_check() {
    $cmd = "net view \\\\" . $this->host_name;
    exec($cmd, $this->net_view_result, $rc);
    return ($rc == 0);
  } // connect_share_check

  function connect_share($action) {
    global $cfg;
    // В любом случае --- доступность компа проверяем по NET VIEW
    if (!$this->connect_share_check()) {
      return 3;
    }

    // Получаем список шар
    $this->shares = array();
    $parse_net = !$cfg['only_admin_shares'];

    if ($cfg['only_admin_shares']) {
      // Если у нас только админские шары --- воспользуемся rmtshare, 
      // который, в отличие от net view даёт полный список шар, включая невидимые

      exec("rmtshare \\\\" . $this->host_name, $rmt_result, $rc);
      reset($rmt_result);
      while(list(, $line) = each($rmt_result)) {

        if ('The command failed: 124' == $line) {
          $parse_net = true;
          break;
        }
        // Только админские шары дисков
        if (preg_match('/^(\w\$)\s/', $line, $m)) {
          $this->shares[] = $m[1];
        // Или шары из одной буквы
        } elseif (preg_match('/^(\w)\s/', $line, $m)) {
          $this->shares[] = $m[1];
        }
      }
    } 
    
    if ($parse_net) {
      // Иначе --- разбираем результат
      reset($this->net_view_result);
      while(list(, $line) = each($this->net_view_result)) {
        if (preg_match('/^([^ ]+)\s+(Disk)\s*(.*)$/', $line, $m)) {
          $this->shares[] = convert_cyr_string($m[1], 'd', 'w');
        }
      }
    }

    if ($cfg['only_admin_shares']) {
      // Удалим шары из одной буквы, если есть шары с этой буквой плюс бакс
      reset($this->shares);
      while(list($key, $sharename) = each($this->shares)) {
        if (strlen($sharename) == 1 && in_array(strtoupper($sharename) . '$', $this->shares)) {
          unset($this->shares[$key]);
        }
      }
    }

    if (0 == count($this->shares)) {
      return 109;
    }

    if ((0 == $action) || (1 == $action)) {
      $param = '';
    } elseif(2 == $action) {
      $param = '/S';
    }

    if ($cfg['index_owners']) {
      $param = '/Q ' . $param;
    }
    
    $this->rawlist = array();
    reset($this->shares);
    $host = $this->host_name;

    // Манипуляции с локалью
    // Иначе будут глюки с потерей буквы 'a' в конце строки
    $old_locale = setlocale(LC_CTYPE, '');
    setlocale(LC_CTYPE, 'Russian_Russia.866'); 

    while(list(, $share_name) = each($this->shares)) {
      $cmd = "dir \"\\\\$host\\$share_name\" /A: $param";
      exec($cmd, $this->rawlist, $return);
    }

    setlocale(LC_CTYPE, $old_locale);

    $this->rawlist_count = count($this->rawlist);

    // Если после индексирования комп не доступен, 
    // значит он мог полностью не проиндексироваться,
    // в этом случае говорим об ошибке
    if (!$this->connect_share_check()) {
      return 3;
    }

    return 0;
  } // connect_share

  function log_action($hact_type, $hact_datetime = '') {
    if ($this->host_id > 0) {
      log_host_action($this->host_id, $hact_type, $hact_datetime);
    }
  } // log_action

  function get_error_text() {
    return get_host_error($this->conn_error);
  } // get_error_text

  function check_ftp_listing($ftp) {
    return 0;
  } // check_ftp_listing

  function resume_ftp_listing($ftp) {
    return 0;
  } // resume_ftp_listing

} // CHost

function get_host_error($conn_error) {
  $error_name = 'L_HOST_ERROR_' . (integer)$conn_error;
  
  if (defined($error_name)) {
    return constant($error_name);
  } else {
    return sprintf(L_HOST_ERROR_UNKNOWN, $conn_error);
  }
} // get_host_error

$host_type_from_mnemo = array(
  'ftp' => HOST_TYPE_FTP,
  'share' => HOST_TYPE_SHARE
);

/**
 * Get numeric host type form its mnemonic
 * using $host_type_from_mnemo
 * If host type for this mnemo does not exists — returns type of ftp
 *
 * @param string $mnemo
 * @return integer
 */
function get_host_type_from_mnemo($mnemo) {
  global $host_type_from_mnemo;
  if (isset($host_type_from_mnemo[$mnemo])) {
    return $host_type_from_mnemo[$mnemo];
  } else {
    return HOST_TYPE_FTP;
  }
} // get_host_type_from_mnemo
?>